using System;
using System.Collections.Generic;
using FluentNHibernate;
using FluentNHibernate.AutoMap;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Context;
using NHibernate.Event;
using Sample.Persistence;

namespace Sample.Service
{
    /// <summary>
    /// Simple NHibernate Unit Of Work class that will work in ASP.NET and Unit Tests.
    /// 
    /// Usage:
    /// 
    /// GLOBAL.ASCX.CS
    /// 
    /// void Application_Start(object sender, EventArgs e)
    /// {
    ///    UoW.Configure("server=local;catalog=test;");
    /// }
    /// 
    /// void Application_BeginRequest(object sender, EventArgs e)
    /// {
    ///   UoW.Start();
    /// }
    /// 
    /// void Application_EndRequest(object sender, EventArgs e)
    /// {
    ///   if(UoW.IsStarted)
    ///       UoW.End();
    /// }
    /// 
    /// </summary>
    /// <remarks>Taken almost verbatim from http://gist.github.com/108433 but 
    /// tweaked to work with existing <see cref="NHibernateHelper"/></remarks>
    public static class UoW
    {
        #region Environment enum

        /// <summary>
        /// What environment do we want to run? Web or Other (suitable for unit tests, desktop clients or remoting)
        /// </summary>
        public enum Environment
        {
            Web,
            Other,
            Test
        }

        #endregion

        /// <summary>
        /// NHibernate session factory is expensive to create.
        /// Therefore we keep it as a static variable so we only create it once.
        /// It's thread safe, so static if fine for both web and desktop.
        /// </summary>
        private static ISessionFactory _nhibernateSessionFactory;

        /// <summary>
        /// We stash the configuration here just to be helpful, in case the app needs it.
        /// This is optional really.
        /// </summary>
        public static Configuration NHibernateConfiguration { get; private set; }

        /// <summary>
        /// Get the session. You must Start the Unit of Work before calling this.
        /// </summary>
        public static ISession Session
        {
            get { return _nhibernateSessionFactory.GetCurrentSession(); }
        }

        public static bool IsStarted
        {
            get
            {
                if (_nhibernateSessionFactory == null) return false;
                return CurrentSessionContext.HasBind(_nhibernateSessionFactory);
            }
        }


        /// <summary>
        /// Get the session factory. Can be accessed once has be created.
        /// </summary>
        public static ISessionFactory SessionFactory
        {
            get { return _nhibernateSessionFactory; }
        }

        /// <summary>
        /// Constructor. We'd only create One UoW class for the entire application (one per ASP.NET App Pool)
        /// </summary>
        /// <param name="nhibernateConfiguration"></param>
        public static void Configure(Configuration nhibernateConfiguration)
        {
            NHibernateConfiguration = NHibernateHelper.Configure();
            _nhibernateSessionFactory = nhibernateConfiguration.BuildSessionFactory();
        }

        public static Configuration Configure(string connectionString, Environment env)
        {
            return Configure(connectionString, env, x => {});
        }
        
        /// <summary>
        /// Simpler constructor. We'd only create One UoW class for the entire application (one per ASP.NET App Pool)
        /// </summary>
        /// <param name="connectionString"></param>
        /// <param name="env">Where will this UoW be used? Web? </param>
        public static Configuration Configure(string connectionString, Environment env, Action<Configuration> configurationAction)
        {
            NHibernateConfiguration = new Configuration();
			
            //we will poke some additional properties into the config
            var additionalProperties = new Dictionary<string, string>();


            //the first is the session context. This allows NHibernate to take care of where it's being used.
            //See NHibernate in Action page 327
            if (env == Environment.Web)
            {
                additionalProperties.Add("current_session_context_class", typeof (WebSessionContext).FullName);
            }
            else
                additionalProperties.Add("current_session_context_class", typeof(CallSessionContext).FullName);

            additionalProperties.Add("use_proxy_validator", "false");

            //additionalProperties.Add("query.substitutions", "true 1, false 0, True 1, False 0, yes 'Y', no 'N', bw_or ^, bw_and &, bw_not ~");

            AutoPersistenceModel mappings = MyMappings.Create();

            if (env == Environment.Test && string.IsNullOrEmpty(connectionString))
            {
                SQLiteConfiguration
                    .Standard
                    .InMemory()
                    .QuerySubstitutions("true 1, false 0, True 1, False 0, yes 'Y', no 'N', bw_or ^, bw_and &, bw_not ~")
                    //.UsingFile("test.db")
                    .ShowSql()
                    .ConfigureProperties(NHibernateConfiguration)
                    .AddProperties(additionalProperties)
                    .AddAutoMappings(mappings)
                    
                    
                    //.SetListener(ListenerType.FlushEntity, new CustomSaveEventListener())
                    ;
//				if(Directory.Exists("c:\\Mappings"))
//				    mappings.WriteMappingsTo("c:\\Mappings\\");
//				else
//				    Console.Write("Not dumping mappings to file for preview because no c:\\Mappings folder.");

            }
            else
            {
                //use Fluent to fluently configure the database
                //you'd have to change this if using MySQL for example.
                MsSqlConfiguration
                    .MsSql2005
                    .ConnectionString(c => c.Is(connectionString))
                    .QuerySubstitutions("true 1, false 0, True 1, False 0, yes 'Y', no 'N', bw_or ^, bw_and &, bw_not ~")
                    .ShowSql()
                    .ConfigureProperties(NHibernateConfiguration)
                    .AddProperties(additionalProperties)
                    .AddAutoMappings(mappings)
                    .SetProperty("generate_statistics", "true")
                    .SetListener(ListenerType.FlushEntity, new CustomSaveEventListener())
                    ;
            }

            NHibernateConfiguration.SetListener(ListenerType.Update, new CustomUpdateEventListener());
            NHibernateConfiguration.SetListener(ListenerType.SaveUpdate, new CustomSaveOrUpdateEventListener());


            configurationAction.Invoke(NHibernateConfiguration);

            //set the static session factory (that will live for as long as the AppPool does
            _nhibernateSessionFactory = NHibernateConfiguration.BuildSessionFactory();
			
            return NHibernateConfiguration;
        }
        
        /// <summary>
        /// Start a new unit of work. 
        /// </summary>
        public static void Start()
        {
            if(_nhibernateSessionFactory == null) return;
            //stash a new session where we can get at it at any time. NHibernate already 
            //has a CurrentSessionContext class for keeping sessions somewhere accessible.
            CurrentSessionContext.Bind(_nhibernateSessionFactory.OpenSession());

            //We don't want to push any work to the DB unless we explicitly call Commit.
            Session.FlushMode = FlushMode.Commit;

            //all our work should be in a transaction
            Session.BeginTransaction();
        }

        /// <summary>
        /// Send all work done in the UoW's transaction to the database. Also starts a new transaction 
        /// ready for any additional work you do.
        /// You can call this method multiple times for during a single Unit of Work 
        /// if you want to send several updates separately.
        /// </summary>
        public static void Commit()
        {
            Session.Transaction.Commit();
            Session.BeginTransaction();
        }

        /// <summary>
        /// End the current unit of work.
        /// </summary>
        public static void End()
        {
            //We want to roll back any active transaction. If the user of this class wanted to commit 
            //any updates then he should have to have called UoW.Commit().
            if (Session.Transaction != null && Session.Transaction.IsActive) Session.Transaction.Rollback();

            //close the session
            Session.Close();

            //Session is disposable, and therefor it's good to call Dispose() once we're done with it.
            Session.Dispose();

            //unbind the session from the context as we won't be needing it. 
            CurrentSessionContext.Unbind(_nhibernateSessionFactory);
        }
    }
}